package service

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"inventory/api/inventory"
	"inventory/internal/constant"
	"inventory/pkg/orm"
	"inventory/pkg/util"
	"strconv"
	"strings"
	"time"

	"inventory/internal/model"
	"inventory/internal/pkg/generator"
	"inventory/pkg/ecode"
)

// GetInventoryList 获取盘点列表
func (s *Service) GetInventoryList(ctx context.Context, req *inventory.GetInventoryListRequest) (resp *inventory.GetInventoryListResponse, err error) {
	var (
		inventoryOrders    []*model.InventoryOrder
		views              []*inventory.InventoryListView
		total              int64
		endDate, startDate time.Time
	)
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if startDate, endDate, err = verifyStartDateAndEndDate(req.StartDate, req.EndDate); err != nil {
		return
	}
	if _, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.ReviewInventory); err != nil {
		return
	}
	if total, inventoryOrders, err = s.dao.FindInventoryOrders(
		ctx, sgId, req.Id, startDate, endDate, int(req.PageNo), int(req.PageSize), req.QueryOrderNumber); err != nil {
		return
	}
	views = make([]*inventory.InventoryListView, len(inventoryOrders))
	for i, o := range inventoryOrders {
		views[i] = &inventory.InventoryListView{
			OrderNumber: o.OrderNumber,
			CreateTime:  o.CreateTime.Format(util.DefaultLayout),
			DetailCount: uint32(o.DetailCount),
			Status:      uint32(o.Status),
		}
		if o.SubmitTime != nil {
			views[i].SubmitTime = o.SubmitTime.Format(util.DefaultLayout)
		}
	}
	resp = &inventory.GetInventoryListResponse{
		Total:  uint32(total),
		Orders: views,
	}
	return
}

// GetInventoryInfo 获取盘点详情
func (s *Service) GetInventoryInfo(ctx context.Context, req *inventory.CommonOrderRequest) (resp *inventory.GetInventoryInfoResponse, err error) {
	var (
		inventoryDetails []*model.InventoryDetail
		inventoryOrder   *model.InventoryOrder
		warehouse        *model.Warehouse
		views            []*inventory.InventoryInfo
	)
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.ReviewInventory); err != nil {
		return
	}
	if inventoryOrder, err = s.dao.GetInventoryOrder(ctx, int(req.Id), req.OrderNumber); err != nil {
		if errors.Cause(err) == gorm.ErrRecordNotFound {
			err = constant.ErrOrderNotExist
		}
		return
	}
	if inventoryDetails, err = s.dao.FindInventoryOrderDetails(ctx, inventoryOrder.ID); err != nil {
		return
	}
	views = make([]*inventory.InventoryInfo, len(inventoryDetails))
	for i, o := range inventoryDetails {
		views[i] = &inventory.InventoryInfo{
			StorageType:     inventory.StorageType(o.StorageType),
			StorageKey:      o.StorageKey,
			InputQuantity:   o.InputQuantity,
			StorageQuantity: o.StorageQuantity,
			DiffQuantity:    o.DiffQuantity,
			Remark:          o.Remark,
		}
	}
	resp = &inventory.GetInventoryInfoResponse{
		OrderNumber:   inventoryOrder.OrderNumber,
		Status:        uint32(inventoryOrder.Status),
		CreateTime:    inventoryOrder.CreateTime.Format(util.DefaultLayout),
		WarehouseName: warehouse.Name,
		Submitter:     s.downstream.GetEmployeeName(ctx, sgId, inventoryOrder.Submitter),
		Data:          views,
	}
	if inventoryOrder.SubmitTime != nil {
		resp.SubmitTime = inventoryOrder.SubmitTime.Format(util.DefaultLayout)
	}
	return
}

func (s *Service) addInventoryInternal(ctx context.Context, sgID uint32, warehouse *model.Warehouse, operator uint32,
	isSubmitted bool, lines []*inventory.InventoryLine, productRemarks, materialRemarks map[string]*model.FullRemark,
	memo string) (orderNumber string, err error) {
	now := time.Now()
	var inventoryOrder *model.InventoryOrder
	if orderNumber, err = generator.GenerateOrderNumber(now, constant.OrderTypeInventory, operator); err != nil {
		return
	}
	inventoryOrder = &model.InventoryOrder{
		OrderNumber: orderNumber,
		WarehouseID: warehouse.ID,
		SgID:        sgID,
		Status:      constant.InventoryOrderProcessing,
		DetailCount: len(lines),
		Memo:        memo,
		CreateTime:  now,
		UpdateTime:  now,
	}
	if isSubmitted {
		inventoryOrder.Submit(operator, now)
	}
	if err = s.dao.Create(ctx, &inventoryOrder); err != nil {
		return
	}
	if err = s.rebuildInventoryDetails(ctx, warehouse, lines, inventoryOrder, productRemarks, materialRemarks); err != nil {
		return
	}
	return
}

func (s *Service) verifyInventoryData(ctx context.Context, sgID, pStoreID uint32,
	lines []*inventory.InventoryLine) (productRemarks, materialRemarks map[string]*model.FullRemark, err error) {
	var (
		materialID  int
		materialIDs []uint64
	)
	// group ids
	objectKeyMap := make(util.StringBoolMap)
	for _, d := range lines {
		if d.InputQuantity < 0 {
			err = ecode.BadRequest("盘点数量不能小于0")
			return
		}
		switch d.StorageType {
		case inventory.StorageType_PROD:
			objectKeyMap[d.StorageKey] = true
		case inventory.StorageType_MATERIAL:
			if materialID, err = strconv.Atoi(strings.Split(d.StorageKey, "-")[1]); err != nil {
				err = ecode.BadRequest("物料Storage_key格式错误")
				return
			}
			materialIDs = append(materialIDs, uint64(materialID))
		default:
			err = ecode.BadRequest("存在不支持的存储类型")
			return
		}
	}
	// build remarks
	if len(objectKeyMap) != 0 {
		if productRemarks, err = s.downstream.BuildObjectRemarks(ctx, sgID, objectKeyMap.GetKeys()); err != nil {
			return
		}
		if len(productRemarks) != len(objectKeyMap.GetKeys()) {
			err = ecode.BadRequest("请求中存在无效的商品SKU")
			return
		}
		for _, o := range productRemarks {
			if pStoreID != 0 && o.PStoreID != 0 && pStoreID != o.PStoreID {
				err = ecode.BadRequest("当前所选商品不可用")
				return
			}
		}
		var boms []*model.BomConfig
		if boms, err = s.dao.FindBomByStorageKeys(ctx, objectKeyMap.GetKeys()); err != nil {
			return
		}
		if len(boms) != 0 {
			prodNameMap := make(util.StringBoolMap)
			for _, bom := range boms {
				prodNameMap[productRemarks[bom.StorageKey].Name] = true
			}
			err = ecode.AppendMessage(constant.ErrForbiddenAction,
				fmt.Sprintf("所选商品(%s)已配置bom，不能进行盘点", strings.Join(prodNameMap.GetKeys(), ",")))
			return
		}
	}
	if len(materialIDs) != 0 {
		if materialRemarks, err = s.dao.BuildMaterialsRemarks(ctx, sgID, materialIDs); err != nil {
			return
		}
		if len(materialRemarks) != len(materialIDs) {
			err = ecode.BadRequest("请求中存在无效的产品ID")
			return
		}
		for _, o := range materialRemarks {
			if pStoreID != 0 && o.PStoreID != 0 && pStoreID != o.PStoreID {
				err = ecode.BadRequest("当前所选产品不可用")
				return
			}
		}
	}
	return
}

// AddInventoryDraft 添加盘点草稿
func (s *Service) AddInventoryInfo(ctx context.Context, req *inventory.AddInventoryInfoRequest) (resp *inventory.CommonOrderReply, err error) {
	var (
		productRemarks, materialRemarks map[string]*model.FullRemark
	)
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if productRemarks, materialRemarks, err = s.verifyInventoryData(
		ctx, sgId, req.PStoreId, req.Data); err != nil {
		return
	}
	resp = &inventory.CommonOrderReply{}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var warehouse *model.Warehouse
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.MakeInventory); err != nil {
			return
		}
		if resp.OrderNumber, err = s.addInventoryInternal(
			ctx, sgId, warehouse, empId, req.IsSubmitted, req.Data, productRemarks, materialRemarks, ""); err != nil {
			return
		}
		return
	})
	return
}

// EditInventoryDraft 修改盘点草稿
func (s *Service) EditInventoryDraft(ctx context.Context, req *inventory.EditInventoryDraftRequest) (resp *inventory.DefaultResp, err error) {
	var productRemarks, materialRemarks map[string]*model.FullRemark
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if productRemarks, materialRemarks, err = s.verifyInventoryData(
		ctx, sgId, req.PStoreId, req.Data); err != nil {
		return
	}
	resp = &inventory.DefaultResp{
		Status: true,
	}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var warehouse *model.Warehouse
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.MakeInventory); err != nil {
			return
		}
		var (
			inventoryOrder *model.InventoryOrder
		)
		now := time.Now()
		inventoryOrder = &model.InventoryOrder{}
		if err = s.dao.Lock(ctx, model.InventoryOrder{
			OrderNumber: req.OrderNumber,
			SgID:        sgId,
			Status:      constant.InventoryOrderProcessing,
			WarehouseID: req.Id,
		}, inventoryOrder); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				return constant.ErrOrderNotExist
			}
			return
		}
		if inventoryOrder.Status != constant.InventoryOrderProcessing {
			err = constant.ErrNeedRefresh
			return
		}
		inventoryOrder.UpdateTime = now
		if req.IsSubmitted {
			inventoryOrder.Submit(empId, now)
		}
		if err = s.dao.DeleteAllInventoryDraftByOrderID(ctx, inventoryOrder.ID); err != nil {
			return err
		}
		if err = s.rebuildInventoryDetails(ctx, warehouse, req.Data, inventoryOrder, productRemarks, materialRemarks); err != nil {
			return
		}
		// save inventoryOrder
		if err = s.dao.Save(ctx, inventoryOrder); err != nil {
			return
		}
		return
	})
	return
}

func (s *Service) rebuildInventoryDetails(ctx context.Context, warehouse *model.Warehouse, orderLines []*inventory.InventoryLine,
	inventoryOrder *model.InventoryOrder, productRemarks, materialRemarks map[string]*model.FullRemark) (err error) {
	// create details
	var (
		remark     *model.FullRemark
		remarkJson json.RawMessage
		storages   []*model.Storage
		priceMap   map[string]uint64
		diffTotal  int64
	)
	storageKeyMap := make(util.StringBoolMap)
	for _, d := range orderLines {
		storageKeyMap[d.StorageKey] = true
	}
	storageMap := make(model.StorageMap)
	if storages, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, warehouse.ID, storageKeyMap.GetKeys()); err != nil {
		return
	}
	storageMap.InitStorageMap(storages)
	if priceMap, err = s.dao.GetLastMonthStoragePriceByStorageKeys(ctx, warehouse.ID, storageKeyMap.GetKeys(),
		inventoryOrder.UpdateTime, materialRemarks, constant.UnitTypeStorage); err != nil {
		return
	}
	materialMap := make(util.UInt64BoolMap)
	for _, d := range orderLines {
		var (
			prodPointer, materialPointer *uint64
			storageQuantity              float64
		)
		switch d.StorageType {
		case inventory.StorageType_MATERIAL:
			remark = materialRemarks[d.StorageKey]
			if tmpID, e := strconv.Atoi(strings.Split(d.StorageKey, "-")[1]); e != nil {
				err = e
				return
			} else {
				materialID := uint64(tmpID)
				materialMap[materialID] = true
				materialPointer = &materialID
			}

		case inventory.StorageType_PROD:
			remark = productRemarks[d.StorageKey]
			if tmpID, e := strconv.Atoi(strings.Split(d.StorageKey, "-")[0]); e != nil {
				err = e
				return
			} else {
				prodID := uint64(tmpID)
				prodPointer = &prodID
			}
		default:
			err = constant.ErrSystemError
			return
		}
		if remarkJson, err = json.Marshal(model.StorageSnapShotRemark{
			SnapShotRemark: remark.SnapShotRemark,
			StorageUnit:    remark.StorageUnit,
			StorageRatio:   remark.StorageRatio,
		}); err != nil {
			return
		}
		if storage, ok := storageMap[d.StorageKey]; ok {
			storageQuantity = storage.Quantity
		}
		price, _ := priceMap[d.StorageKey]
		diffQuantity := d.InputQuantity - storageQuantity
		diffTotal += int64(float64(price) * diffQuantity / remark.StorageRatio)
		if err = s.dao.Create(ctx, &model.InventoryDetail{
			OrderID:         inventoryOrder.ID,
			WarehouseID:     inventoryOrder.WarehouseID,
			StorageType:     uint8(d.StorageType),
			StorageKey:      d.StorageKey,
			Status:          inventoryOrder.Status,
			Remark:          orm.JSON(remarkJson),
			InputQuantity:   d.InputQuantity,
			StorageQuantity: storageQuantity,
			DiffQuantity:    diffQuantity,
			Price:           price,
			UpdateTime:      inventoryOrder.UpdateTime,
		}); err != nil {
			return
		}
		if inventoryOrder.Status == constant.InventoryOrderDone {
			if remarkJson, err = json.Marshal(model.StorageRealTimeRemark{
				Remark:        remark.Remark,
				Type:          remark.TypeID,
				StorageUnit:   remark.StorageUnit,
				StorageRatio:  remark.StorageRatio,
				PurchaseUnit:  remark.PurchaseUnit,
				PurchaseRatio: remark.PurchaseRatio,
			}); err != nil {
				return
			}
			if err = s.dao.CreateOrCoverStorage(ctx, &model.Storage{
				WarehouseID:    warehouse.ID,
				StorageKey:     d.StorageKey,
				StorageType:    uint8(d.StorageType),
				Quantity:       d.InputQuantity,
				FreezeQuantity: 0,
				StorageMode:    warehouse.StorageMode,
				ProdID:         prodPointer,
				MaterialID:     materialPointer,
				Remark:         orm.JSON(remarkJson),
				UpdateTime:     inventoryOrder.UpdateTime,
			}); err != nil {
				return
			}
			// 更新日结记录
			if err = s.dao.Create(ctx, &model.StatsDaily{
				RecordDate:    inventoryOrder.UpdateTime,
				ActionType:    constant.ActionInventory,
				WarehouseID:   warehouse.ID,
				StorageKey:    d.StorageKey,
				StorageType:   uint8(d.StorageType),
				StorageName:   remark.Name,
				Quantity:      d.InputQuantity - storageQuantity,
				Price:         int64(price),
				OperationUnit: remark.StorageUnit,
				Amount:        int64(float64(price) * (d.InputQuantity - storageQuantity) / remark.StorageRatio),
			}); err != nil {
				return
			}
		}
	}
	if diffTotal != 0 {
		inventoryOrder.DiffTotal = diffTotal
		if err = s.dao.Save(ctx, inventoryOrder); err != nil {
			return
		}
	}
	return
}

// CancelInventoryDraft 取消盘点草稿
func (s *Service) CancelInventoryDraft(ctx context.Context, req *inventory.CommonOrderRequest) (resp *inventory.DefaultResp, err error) {
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	resp = &inventory.DefaultResp{
		Status: true,
	}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		if _, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.MakeInventory); err != nil {
			return
		}
		inventoryOrder := model.InventoryOrder{}
		if err = s.dao.Lock(ctx, model.InventoryOrder{
			OrderNumber: req.OrderNumber,
			SgID:        sgId,
			Status:      constant.InventoryOrderProcessing}, &inventoryOrder); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				return constant.ErrOrderNotExist
			}
		}
		if inventoryOrder.WarehouseID != req.Id {
			err = constant.ErrPermissionError
			return
		}
		if inventoryOrder.Status == constant.InventoryOrderCanceled {
			return
		}
		now := time.Now()
		inventoryOrder.Status = constant.InventoryOrderCanceled
		inventoryOrder.UpdateTime = now
		if err = s.dao.Save(ctx, inventoryOrder); err != nil {
			return err
		}
		if err = s.dao.UpdateInventoryDetailByOrderID(ctx, inventoryOrder.ID, map[string]interface{}{
			"status":      constant.InventoryOrderCanceled,
			"update_time": now,
		}); err != nil {
			return
		}
		return
	})
	return
}
